//---------------------------------------------------------------------
// <copyright file="EFOracleProviderManifest.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
//---------------------------------------------------------------------

namespace EFOracleOdpProvider
{
    using System.Xml;
    using System.Data.Common;
    using System.Diagnostics;
    using System.Data.Metadata.Edm;
    using System.Data;
    using System;
    using System.Reflection;
    using System.IO;
    using System.Data.Common.Utils;

    /// <summary>
    /// The Provider Manifest for EFOracleOdpProvider
    /// </summary>
    internal class EFOracleOdpProviderManifest : DbXmlEnabledProviderManifest
    {
        #region Internal Fields
        
        internal const string ProviderInvariantName = "EFOracleOdpProvider";

        internal const string TokenOracle10g = "10g";
        internal const string TokenOracle11g = "11g";

        #endregion

        #region Private Fields

        // Default to Oracle 11g
        private EFOracleVersion _version = EFOracleVersion.Oracle11g;
        private string _token = TokenOracle10g;

        private System.Collections.ObjectModel.ReadOnlyCollection<PrimitiveType> _primitiveTypes = null;
        private System.Collections.ObjectModel.ReadOnlyCollection<EdmFunction> _functions = null;

        private const int BinaryMaxSize = 2000;
        private const int Nvarchar2MaxSize = 2000;
        private const int Varchar2MaxSize = 4000;

        #endregion

        #region Constructors
               
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="manifestToken">A token used to infer the capabilities of the store</param>
        public EFOracleOdpProviderManifest(string manifestToken)
            : base(EFOracleOdpProviderManifest.GetProviderManifest())
        {
            // GetSqlVersion will throw ArgumentException if manifestToken is null, empty, or not recognized.
            _version = EFOracleVersionUtils.GetStorageVersion(manifestToken);
            _token = manifestToken;
        }


        #endregion

        #region Properties
                
        /// <summary>
        /// Provider Manifest Token
        /// </summary>
        internal string Token
        {
            get 
            {
                return _token;
            }
        }

        #endregion

        #region Methods

        private static XmlReader GetProviderManifest()
        {
            return GetXmlResource("EFOracleOdpProvider.Resources.EFOracleOdpProviderManifest.xml");
        }

        /// <summary>
        /// Providers should override this to return information specific to their provider.  
        /// 
        /// This method should never return null.
        /// </summary>
        /// <param name="informationType">The name of the information to be retrieved.</param>
        /// <returns>An XmlReader at the begining of the information requested.</returns>
        protected override XmlReader GetDbInformation(string informationType)
        {
            if (informationType == DbProviderManifest.StoreSchemaDefinition)
            {
                return GetStoreSchemaDescription();
            }

            if (informationType == DbProviderManifest.StoreSchemaMapping)
            {
                return GetStoreSchemaMapping();
            }

            // Use default Conceptual Schema Definition
            if (informationType == DbProviderManifest.ConceptualSchemaDefinition)
            {
                return null;
            }

            throw new ProviderIncompatibleException(
                                string.Format("The provider returned null for the informationType '{0}'.",
                                    informationType)
                            );
        }

        public override System.Collections.ObjectModel.ReadOnlyCollection<PrimitiveType> GetStoreTypes()
        {            
            if (this._primitiveTypes == null)
            {
                if (EFOracleVersionUtils.IsVersionX(this._version))
                {
                    this._primitiveTypes = base.GetStoreTypes();
                }
                else
                {
                    //
                    // CONSIDER removing unsupported types
                    //

                    /*
                    List<PrimitiveType> primitiveTypes = new List<PrimitiveType>(base.GetStoreTypes());
                    primitiveTypes.RemoveAll(new Predicate<PrimitiveType>(
                                                        delegate(PrimitiveType primitiveType)
                                                        {
                                                            string name = primitiveType.Name.ToLowerInvariant();
                                                            return name.Equals("type1", StringComparison.Ordinal) ||
                                                                   name.Equals("type2", StringComparison.Ordinal);
                                                        }
                                                    )
                                        );
                    this._primitiveTypes = primitiveTypes.AsReadOnly();
                    */

                    Debug.Fail("Unsupported version: " + _version);
                }
            }

            return this._primitiveTypes;
        }

        public override System.Collections.ObjectModel.ReadOnlyCollection<EdmFunction> GetStoreFunctions()
        {
            if (this._functions == null)
            {
                if (EFOracleVersionUtils.IsVersionX(this._version))
                {
                    this._functions = base.GetStoreFunctions();
                }
                else
                {
                    //
                    // CONSIDER removing unsupported functions
                    //

                    /*
                    List<EdmFunction> functions = new List<EdmFunction>(base.GetStoreFunctions());
                    functions.RemoveAll(new Predicate<EdmFunction>(
                                    delegate(EdmFunction edmFunction)
                                    {
                                        ReadOnlyMetadataCollection<FunctionParameter> funParams = edmFunction.Parameters;
                                        switch (edmFunction.Name.ToUpperInvariant())
                                        {
                                            case "FUNC1":
                                            case "FUNC2":
                                                {
                                                    string name = ((CollectionType)funParams[0].TypeUsage.EdmType).TypeUsage.EdmType.Name;
                                                    return ((name.Equals("type1", StringComparison.OrdinalIgnoreCase)) ||
                                                           (name.Equals("type2", StringComparison.OrdinalIgnoreCase)));

                                                }
                                        }

                                        return false;
                                    }
                                )
                    );                        
                    this._functions = functions.AsReadOnly();
                    */
                }
            }

            return this._functions;
        }

        /// <summary>
        /// This method takes a type and a set of facets and returns the best mapped equivalent type 
        /// in EDM.
        /// </summary>
        /// <param name="storeType">A TypeUsage encapsulating a store type and a set of facets</param>
        /// <returns>A TypeUsage encapsulating an EDM type and a set of facets</returns>
        public override TypeUsage GetEdmType(TypeUsage storeType)
        {
            EntityUtils.CheckArgumentNull(storeType, "storeType");

            string storeTypeName = storeType.EdmType.Name.ToLowerInvariant();
            if (!base.StoreTypeNameToEdmPrimitiveType.ContainsKey(storeTypeName))
            {
                throw new ArgumentException(string.Format(
                            "The underlying provider does not support the type '{0}'.", storeTypeName));
            }

            PrimitiveType edmPrimitiveType = base.StoreTypeNameToEdmPrimitiveType[storeTypeName];

            int maxLength = 0;
            bool isUnicode = true;
            bool isFixedLen = false;
            bool isUnbounded = true;

            PrimitiveTypeKind newPrimitiveTypeKind;

            switch (storeTypeName)
            {
                // for some types we just go with simple type usage with no facets
                case "int":
                case "smallint":                
                    return TypeUsage.CreateDefaultTypeUsage(edmPrimitiveType);

                case "ref cursor":
                    return TypeUsage.CreateBinaryTypeUsage(edmPrimitiveType, true, 12345);

                case "varchar2":
                    newPrimitiveTypeKind = PrimitiveTypeKind.String;
                    isUnbounded = !MetadataHelpers.TryGetMaxLength(storeType, out maxLength);
                    isUnicode = false;
                    isFixedLen = false;
                    break;

                case "char":
                    newPrimitiveTypeKind = PrimitiveTypeKind.String;
                    isUnbounded = !MetadataHelpers.TryGetMaxLength(storeType, out maxLength);
                    isUnicode = false;
                    isFixedLen = true;
                    break;

                case "nvarchar2":
                    newPrimitiveTypeKind = PrimitiveTypeKind.String;
                    isUnbounded = !MetadataHelpers.TryGetMaxLength(storeType, out maxLength);
                    isUnicode = true;
                    isFixedLen = false;
                    break;

                case "nchar":
                    newPrimitiveTypeKind = PrimitiveTypeKind.String;
                    isUnbounded = !MetadataHelpers.TryGetMaxLength(storeType, out maxLength);
                    isUnicode = true;
                    isFixedLen = true;
                    break;

                case "clob":
                    newPrimitiveTypeKind = PrimitiveTypeKind.String;
                    isUnbounded = true;
                    isUnicode = false;
                    isFixedLen = false;
                    break;

                case "nclob":
                    newPrimitiveTypeKind = PrimitiveTypeKind.String;
                    isUnbounded = true;
                    isUnicode = true;
                    isFixedLen = false;
                    break;

                case "blob":
                    newPrimitiveTypeKind = PrimitiveTypeKind.Binary;
                    isUnbounded = true;
                    isFixedLen = false;
                    break;

                case "raw":
                    newPrimitiveTypeKind = PrimitiveTypeKind.Binary;
                    isUnbounded = !MetadataHelpers.TryGetMaxLength(storeType, out maxLength);
                    isFixedLen = false;
                    if (maxLength == 16)
                    {
                        // ##ORACLE TODO map all RAW(16) to Guid
                        return TypeUsage.CreateDefaultTypeUsage(PrimitiveType.GetEdmPrimitiveType(PrimitiveTypeKind.Guid));
                    }
                    break;

                case "long raw":
                    newPrimitiveTypeKind = PrimitiveTypeKind.Binary;
                    isUnbounded = !MetadataHelpers.TryGetMaxLength(storeType, out maxLength);
                    isFixedLen = false;
                    break;

                case "guid":
                case "binary_float":
                case "binary_double":
                    return TypeUsage.CreateDefaultTypeUsage(edmPrimitiveType);

                case "number":
                    {
                        byte precision;
                        byte scale;

                        if (MetadataHelpers.TryGetPrecision(storeType, out precision) &&
                            MetadataHelpers.TryGetScale(storeType, out scale))
                        {
                            if (precision == 1 && scale == 0)
                                return TypeUsage.CreateDefaultTypeUsage(PrimitiveType.GetEdmPrimitiveType(PrimitiveTypeKind.Boolean));

                            if (precision == 5 && scale == 0)
                                return TypeUsage.CreateDefaultTypeUsage(PrimitiveType.GetEdmPrimitiveType(PrimitiveTypeKind.Int16));

                            if (precision == 11 && scale == 0)
                                return TypeUsage.CreateDefaultTypeUsage(PrimitiveType.GetEdmPrimitiveType(PrimitiveTypeKind.Int32));

                            return TypeUsage.CreateDecimalTypeUsage(edmPrimitiveType, precision, scale);
                        }
                        else
                        {
                            return TypeUsage.CreateDecimalTypeUsage(edmPrimitiveType);
                        }
                    }

                case "date":
                    return TypeUsage.CreateDateTimeTypeUsage(PrimitiveType.GetEdmPrimitiveType(
                                    PrimitiveTypeKind.DateTime), null);

                case "timestamp":
                    {
                        byte precision;
                        if (MetadataHelpers.TryGetPrecision(storeType, out precision))
                        {
                            return TypeUsage.CreateDateTimeOffsetTypeUsage(PrimitiveType.GetEdmPrimitiveType(
                                    PrimitiveTypeKind.DateTimeOffset), precision);
                        }
                        else
                        {
                            return TypeUsage.CreateDateTimeOffsetTypeUsage(
                                    PrimitiveType.GetEdmPrimitiveType(PrimitiveTypeKind.DateTimeOffset), null);
                        }
                    }

                default:
                    throw new NotSupportedException(string.Format(
                                "The underlying provider does not support the type '{0}'.", storeTypeName));
            }

            Debug.Assert(newPrimitiveTypeKind == PrimitiveTypeKind.String || newPrimitiveTypeKind == PrimitiveTypeKind.Binary, "at this point only string and binary types should be present");

            switch (newPrimitiveTypeKind)
            {
                case PrimitiveTypeKind.String:
                    if (!isUnbounded)
                    {
                        return TypeUsage.CreateStringTypeUsage(edmPrimitiveType, isUnicode, isFixedLen, maxLength);
                    }
                    else
                    {
                        return TypeUsage.CreateStringTypeUsage(edmPrimitiveType, isUnicode, isFixedLen);
                    }
                case PrimitiveTypeKind.Binary:
                    if (!isUnbounded)
                    {
                        return TypeUsage.CreateBinaryTypeUsage(edmPrimitiveType, isFixedLen, maxLength);
                    }
                    else
                    {
                        return TypeUsage.CreateBinaryTypeUsage(edmPrimitiveType, isFixedLen);
                    }
                default:
                    throw new NotSupportedException(string.Format(
                        "The underlying provider does not support the type '{0}'.", storeTypeName));
            }
        }

        /// <summary>
        /// This method takes a type and a set of facets and returns the best mapped equivalent type 
        /// in Oracle, taking the store version into consideration.
        /// </summary>
        /// <param name="storeType">A TypeUsage encapsulating an EDM type and a set of facets</param>
        /// <returns>A TypeUsage encapsulating a store type and a set of facets</returns>
        public override TypeUsage GetStoreType(TypeUsage edmType)
        {
            EntityUtils.CheckArgumentNull<TypeUsage>(edmType, "edmType");
            System.Diagnostics.Debug.Assert(edmType.EdmType.BuiltInTypeKind == BuiltInTypeKind.PrimitiveType);

            PrimitiveType primitiveType = edmType.EdmType as PrimitiveType;
            if (primitiveType == null)
            {
                throw new ArgumentException(string.Format("The underlying provider does not support the type '{0}'.",
                    edmType));
            }

            ReadOnlyMetadataCollection<Facet> facets = edmType.Facets;

            switch (primitiveType.PrimitiveTypeKind)
            {
                case PrimitiveTypeKind.Boolean:
                case PrimitiveTypeKind.Byte:
                    return TypeUsage.CreateDecimalTypeUsage(StoreTypeNameToStorePrimitiveType["number"], 1, 0);

                case PrimitiveTypeKind.Int16:
                    return TypeUsage.CreateDecimalTypeUsage(StoreTypeNameToStorePrimitiveType["number"], 5, 0);

                case PrimitiveTypeKind.Int32:
                    return TypeUsage.CreateDecimalTypeUsage(StoreTypeNameToStorePrimitiveType["number"], 11, 0);

                case PrimitiveTypeKind.Int64:
                    return TypeUsage.CreateDecimalTypeUsage(StoreTypeNameToStorePrimitiveType["number"], 20, 0);

                case PrimitiveTypeKind.Guid:
                    return TypeUsage.CreateBinaryTypeUsage(StoreTypeNameToStorePrimitiveType["raw"], true, 16);

                case PrimitiveTypeKind.Double:
                    return TypeUsage.CreateDefaultTypeUsage(StoreTypeNameToStorePrimitiveType["binary_double"]);

                case PrimitiveTypeKind.Single:
                    return TypeUsage.CreateDefaultTypeUsage(StoreTypeNameToStorePrimitiveType["binary_float"]);

                case PrimitiveTypeKind.Decimal:
                    {
                        byte precision;
                        byte scale;
                        if (MetadataHelpers.TryGetPrecision(edmType, out precision) && MetadataHelpers.TryGetScale(edmType, out scale))
                        {
                            return TypeUsage.CreateDecimalTypeUsage(StoreTypeNameToStorePrimitiveType["number"], precision, scale);
                        }
                        else
                        {
                            return TypeUsage.CreateDefaultTypeUsage(StoreTypeNameToStorePrimitiveType["number"]);
                        }
                    }

                case PrimitiveTypeKind.Binary:
                    {
                        bool isFixedLength = null != facets[MetadataHelpers.FixedLengthFacetName].Value && (bool)facets[MetadataHelpers.FixedLengthFacetName].Value;
                        Facet f = facets[MetadataHelpers.MaxLengthFacetName];

                        bool isMaxLength = f.IsUnbounded || null == f.Value || (int)f.Value > BinaryMaxSize;
                        int maxLength = !isMaxLength ? (int)f.Value : Int32.MinValue;

                        TypeUsage tu;
                        if (isFixedLength)
                        {
                            tu = TypeUsage.CreateBinaryTypeUsage(StoreTypeNameToStorePrimitiveType["raw"], true, (isMaxLength ? BinaryMaxSize : maxLength));
                        }
                        else
                        {
                            if (isMaxLength)
                            {
                                tu = TypeUsage.CreateBinaryTypeUsage(StoreTypeNameToStorePrimitiveType["blob"], false, BinaryMaxSize);
                            }
                            else
                            {
                                tu = TypeUsage.CreateBinaryTypeUsage(StoreTypeNameToStorePrimitiveType["blob"], false, maxLength);
                            }
                        }
                        return tu;
                    }

                case PrimitiveTypeKind.String:
                    {
                        bool isUnicode = null == facets[MetadataHelpers.UnicodeFacetName].Value || (bool)facets[MetadataHelpers.UnicodeFacetName].Value;
                        bool isFixedLength = null != facets[MetadataHelpers.FixedLengthFacetName].Value && (bool)facets[MetadataHelpers.FixedLengthFacetName].Value;
                        Facet f = facets[MetadataHelpers.MaxLengthFacetName];
                        // maxlen is true if facet value is unbounded, the value is bigger than the limited string sizes *or* the facet
                        // value is null. this is needed since functions still have maxlength facet value as null
                        bool isMaxLength = f.IsUnbounded || null == f.Value || (int)f.Value > (isUnicode ? Nvarchar2MaxSize : Varchar2MaxSize);
                        int maxLength = !isMaxLength ? (int)f.Value : Int32.MinValue;

                        TypeUsage tu;

                        if (isUnicode)
                        {
                            if (isFixedLength)
                            {
                                tu = TypeUsage.CreateStringTypeUsage(StoreTypeNameToStorePrimitiveType["nchar"], true, true, (isMaxLength ? Nvarchar2MaxSize : maxLength));
                            }
                            else
                            {
                                if (isMaxLength)
                                {
                                    tu = TypeUsage.CreateStringTypeUsage(StoreTypeNameToStorePrimitiveType["nclob"], true, false);
                                    Debug.Assert(tu.Facets[MetadataHelpers.MaxLengthFacetName].Description.IsConstant, "nclob is not constant!");
                                }
                                else
                                {
                                    tu = TypeUsage.CreateStringTypeUsage(StoreTypeNameToStorePrimitiveType["nvarchar2"], true, false, maxLength);
                                }
                            }
                        }
                        else    // !isUnicode
                        {
                            if (isFixedLength)
                            {
                                tu = TypeUsage.CreateStringTypeUsage(StoreTypeNameToStorePrimitiveType["char"], false, true,
                                    (isMaxLength ? Varchar2MaxSize : maxLength));
                            }
                            else
                            {
                                if (isMaxLength)
                                {
                                    tu = TypeUsage.CreateStringTypeUsage(StoreTypeNameToStorePrimitiveType["clob"], false, false);
                                    Debug.Assert(tu.Facets[MetadataHelpers.MaxLengthFacetName].Description.IsConstant, "clob is not constant!");
                                }
                                else
                                {
                                    tu = TypeUsage.CreateStringTypeUsage(StoreTypeNameToStorePrimitiveType["varchar2"], false, false, maxLength);
                                }
                            }
                        }
                        return tu;
                    }


                case PrimitiveTypeKind.DateTime:
                    return TypeUsage.CreateDefaultTypeUsage(StoreTypeNameToStorePrimitiveType["date"]);
                case PrimitiveTypeKind.DateTimeOffset:
                case PrimitiveTypeKind.Time:
                    return TypeUsage.CreateDefaultTypeUsage(StoreTypeNameToStorePrimitiveType["timestamp"]);

                default:
                    throw new NotSupportedException(string.Format("There is no store type corresponding to the conceptual side type '{0}' of PrimitiveType '{1}'.", edmType, primitiveType.PrimitiveTypeKind));
            }
        }

        private XmlReader GetStoreSchemaMapping()
        {
            return GetXmlResource("EFOracleOdpProvider.Resources.EFOracleStoreSchemaMapping.msl");
        }

        private XmlReader GetStoreSchemaDescription()
        {
            if (EFOracleVersionUtils.IsVersionX(this._version))
            {
                return GetXmlResource("EFOracleOdpProvider.Resources.EFOracleStoreSchemaDefinition.ssdl");
            }

            Debug.Fail("Unsupported version: " + _version);
            return null;
        }

        internal static XmlReader GetXmlResource(string resourceName)
        {
            Assembly executingAssembly = Assembly.GetExecutingAssembly();
            Stream stream = executingAssembly.GetManifestResourceStream(resourceName);
            return XmlReader.Create(stream, null, resourceName);
        }

        #endregion
    }
}
